﻿using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Threading;
using Valentia.Kinection.DataTypes;
using Valentia.Kinection.Remoting.Proxy;

namespace Valentia.Kinection.Remoting.Helper
{
    /// <summary>
    /// This class will act as remoting client.
    /// </summary>
    public class Client : IDisposable
    {
        #region Declarations

        /// <summary>
        /// Object is used for locking purpose for synchronizing the connect and disconnect operation.
        /// </summary>
        private readonly Object _ConnectSync = new Object();

        /// <summary>
        /// Timer used to process emails in directory.
        /// </summary>
        private System.Timers.Timer updateServerConnectionStatus;

        /// <summary>
        /// Remoting server.
        /// </summary>
        private IRemotingServerObject remoteServer;

        /// <summary>
        /// Remoting proxy.
        /// </summary>
        private RemotingProxy eventProxy;
        
        /// <summary>
        /// Remoting communication channel.
        /// </summary>
        private TcpChannel tcpChan;

        /// <summary>
        /// Provides the implementation for the binary client formatter sink provider.
        /// </summary>
        private BinaryClientFormatterSinkProvider clientProv;
        
        /// <summary>
        ///  Provides the implementation for the server formatter channel sink provider
        ///  that uses the System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.
        /// </summary>
        private BinaryServerFormatterSinkProvider serverProv;

        /// <summary>
        /// Remoting server URI.
        /// </summary>
        private string serverURI = "tcp://localhost:15000/serverExample.Rem";

        /// <summary>
        /// Utility login name for authentication.
        /// </summary>
        private string loginName = string.Empty;

        /// <summary>
        /// Utility password for authentication.
        /// </summary>
        private string password = string.Empty;

        /// <summary>
        /// Client name used by remoting client to connect.
        /// </summary>
        private string clientName = string.Empty;

        /// <summary>
        /// Client end connection port number.
        /// </summary>
        private int portNo = 0;

        /// <summary>
        /// Connection status interval to check the remoting connection status.
        /// </summary>
        private int connectionStatusInterval = 0;

        /// <summary>
        /// Shows remoting client is connected with remoting server.
        /// </summary>
        private bool connected = false;

        #endregion

        #region Delegates

        /// <summary>
        /// Delegate for devices connection information to be sent.
        /// </summary>
        /// <param name="deviceConnectionInfo">Device(s) connectivity information.</param>
        /// <param name="errorMessage">Error message.</param>
        public delegate void DeviceConnectivityInfoEventDlg(List<KinectDeviceInfo> deviceConnectionInfo, String errorMessage);

        /// <summary>
        /// Delegate for clients connection information to be sent.
        /// </summary>
        /// <param name="clientConnectionInfo">Service client(s) connectivity information.</param>
        /// <param name="errorMessage">Error message.</param>
        public delegate void ClientConnectivityInfoEventDlg(List<ServiceClientInfo> clientConnectionInfo, String errorMessage);

        /// <summary>
        /// Delegate for service client and device connectivity.
        /// </summary>    
        /// <param name="connectivityStatus">Service client(s) connectivity information.</param>
        /// <param name="connectedDeviceCount">Connected device(s) count.</param>
        /// <param name="connectedClientCount">Connected service client(s) count.</param> 
        /// <param name="errorMessage">Error message.</param>
        public delegate void ConnectionStatusEventDlg(ConnectivityStatus connectivityStatus, int connectedDeviceCount, int connectedClientCount, String errorMessage);

        #endregion

        #region Events

        /// <summary>
        /// This event will be fired when server needs to send information of devices connection to client.
        /// </summary>
        public event DeviceConnectivityInfoEventDlg DeviceConnectivityInfoEvent;

        /// <summary>
        /// This event will be fired when server needs to send information of client connected to service.
        /// </summary>
        public event ClientConnectivityInfoEventDlg ClientConnectivityInfoEvent;

        /// <summary>
        /// This event will be fired when server needs to send information of service client and device connectivity status.
        /// </summary>
        public event ConnectionStatusEventDlg ConnectionStatusInfoEvent;

        /// <summary>
        /// This event will be fired when server needs to show notification on service manager utility.
        /// </summary>
        public event EventHandler<NotificationReceivedArgs> ShowNotificationReceived;

        /// <summary>
        /// This event will be fired when client server connection is not alive.
        /// </summary>
        public event EventHandler ServerConnectionStatus;

        #endregion

        #region Constructor

        /// <summary>
        ///  Constructor.
        /// </summary>
        /// <param name="clientName">Client name used by remoting client to connect.</param>
        /// <param name="loginName">Utility login name for authentication.</param>
        /// <param name="password">Utility password for authentication.</param>
        /// <param name="kinectRemotingServerURI">Remoting server URI.</param>
        /// <param name="portNo">Client end connection port number.</param>
        /// <param name="connectionStatusInterval">Connection status interval to check the remoting connection status.</param>
        public Client(string clientName, string loginName, string password, string kinectRemotingServerURI, int portNo, int connectionStatusInterval)
        {
            this.Initlialize(clientName, loginName, password, kinectRemotingServerURI, portNo, connectionStatusInterval);

            #region Intializing timer to monitor remoting connnection

            this.updateServerConnectionStatus = new System.Timers.Timer();
            this.updateServerConnectionStatus.Elapsed += new System.Timers.ElapsedEventHandler(IsConnectionAlive);
            this.updateServerConnectionStatus.Interval = this.connectionStatusInterval * 1000;
            this.updateServerConnectionStatus.Start();

            #endregion
        }
        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when timer elapsed, is used to check remoting connection is alive or not.
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="eventArgs">Event args.</param>
        private void IsConnectionAlive(Object sender, EventArgs eventArgs)
        {
            try
            {
                this.updateServerConnectionStatus.Stop();

                eventArgs = null;
                
                if (!this.connected) { return; }

                // Checking connection is alive by sending command to remoting server
                if(!this.remoteServer.IsConnectionAlive())
                {
                    { try { this.Disconnect(); } catch { } }
                    return;
                }

                // Control comes here it shows connection is alive
                eventArgs = new EventArgs();
            }
            catch { try { this.Disconnect(); } catch { } }
            finally
            {
                if (this.updateServerConnectionStatus != null)
                {
                    try
                    {
                        if (this.ServerConnectionStatus != null) { this.ServerConnectionStatus(null, eventArgs); }
                    }
                    catch { }
                    finally
                    {
                        { try { this.updateServerConnectionStatus.Interval = this.connectionStatusInterval * 1000; } catch { } }
                        { try { this.updateServerConnectionStatus.Start(); } catch { } }
                    }
                }
            }
        }

        /// <summary>
        /// Occurs when server sends information of connected service client(s) list.
        /// </summary>
        /// <param name="clientConnectionInfo">List of connected service clients.</param>
        /// <param name="errorMessage">Error Message.</param>
        private void eventProxy_ClientConnectivityInfo(List<ServiceClientInfo> clientConnectionInfo, String errorMessage)
        {
            if (ClientConnectivityInfoEvent != null)
            {
                ClientConnectivityInfoEvent(clientConnectionInfo, errorMessage);
            }
        }

        /// <summary>
        ///  Occurs when server sends information of connected kinect devices list.
        /// </summary>
        /// <param name="deviceConnectionInfo">List of connected devices.</param>
        /// <param name="errorMessage">Error Message.</param>
        private void eventProxy_DeviceConnectivityInfo(List<KinectDeviceInfo> deviceConnectionInfo, String errorMessage)
        {
            if (DeviceConnectivityInfoEvent != null)
            {
                DeviceConnectivityInfoEvent(deviceConnectionInfo, errorMessage);
            }
        }

        /// <summary>
        /// Occurs when server sends information of service client and device connectivity status.
        /// </summary>
        /// <param name="connectivityStatus">Connectivity status of device and service client.</param>
        /// <param name="connectedDeviceCount">Connected devices count.</param>
        /// <param name="connectedClientCount">Connected service clients count.</param>
        /// <param name="errorMessage">Error Message.</param>
        private void eventProxy_ConnectionStatusInfoReceived(ConnectivityStatus connectivityStatus, int connectedDeviceCount
            , int connectedClientCount, String errorMessage)
        {
            if (ConnectionStatusInfoEvent != null)
            {
                ConnectionStatusInfoEvent(connectivityStatus, connectedDeviceCount, connectedClientCount, errorMessage);
            }
        }

        /// <summary>
        ///  Occurs when server sends notification to service manager utility.
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="e">Notfication Event Args.</param>
        private void eventProxy_ShowNotificationReceived(object sender, NotificationReceivedArgs e)
        {
            if (ShowNotificationReceived != null)
            {
                ShowNotificationReceived(sender, e);
            }
        }
        #endregion

        #region Public Methods

        /// <summary>
        /// Initialize the remoting client and server settings.
        /// </summary>
        /// <param name="clientName">Client name used by remoting client to connect.</param>
        /// <param name="loginName">Utility login name for authentication.</param>
        /// <param name="password">Utility password for authentication.</param>
        /// <param name="kinectRemotingServerURI">Remoting server URI.</param>
        /// <param name="portNo">Client end port number.</param>
        /// <param name="connectionStatusInterval">Connection status interval to check the remoting connection status.</param>
        public void Initlialize(string clientName, string loginName, string password, string kinectRemotingServerURI, int portNo, int connectionStatusInterval)
        {
            this.connectionStatusInterval = connectionStatusInterval;

            lock (this._ConnectSync)
            {
                if (this.connected) return;

                this.clientName = clientName;
                this.loginName = loginName;
                this.password = password;
                this.serverURI = kinectRemotingServerURI;
                this.portNo = portNo;
            }
        }

        /// <summary>
        /// Connects with remoting server and register events.
        /// </summary>
        public void Connect()
        {
            bool lockAquired = false;

            try
            {
                if (!Monitor.TryEnter(this._ConnectSync)) return;

                lockAquired = true;

                // if already connected then return
                if (this.connected) return;

                #region Intializting Proxy object and registering events

                this.eventProxy = new RemotingProxy();
                this.eventProxy.KinectDeviceInfoReceived += new DeviceConnectivityInfo(eventProxy_DeviceConnectivityInfo);
                this.eventProxy.ServiceClientInfoReceived += new ClientConnectivityInfo(eventProxy_ClientConnectivityInfo);
                this.eventProxy.ShowNotificationReceived += new EventHandler<NotificationReceivedArgs>(eventProxy_ShowNotificationReceived);
                this.eventProxy.ConnectionStatusInfoReceived += new ConnectionStatus(eventProxy_ConnectionStatusInfoReceived);

                #endregion

                #region Intializing Channel with remoting server

                // Setting properties for channel
                Hashtable props = new Hashtable();
                props["name"] = this.clientName;
                props["port"] = this.portNo;

                this.clientProv = new BinaryClientFormatterSinkProvider();
                this.serverProv = new BinaryServerFormatterSinkProvider();
                this.serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;

                this.tcpChan = new TcpChannel(props, clientProv, serverProv);

                if (!ChannelServices.RegisteredChannels.Contains(this.tcpChan))
                {
                    ChannelServices.RegisterChannel(this.tcpChan, false);
                    WellKnownClientTypeEntry entry = RemotingConfiguration.IsWellKnownClientType(typeof(IRemotingServerObject));

                    if (entry == null)
                    {
                        RemotingConfiguration.RegisterWellKnownClientType(new WellKnownClientTypeEntry(typeof(IRemotingServerObject), serverURI));
                    }
                }
                else
                {
                    // using already registered channel
                    this.tcpChan = ChannelServices.GetChannel(tcpChan.ChannelName) as TcpChannel;
                }

                #endregion

                #region Intializing remote server object and registering events

                this.remoteServer = (IRemotingServerObject)Activator.GetObject(typeof(IRemotingServerObject), serverURI);
                this.remoteServer.CommandFromClient(ClientCommands.ClientConnected, loginName, password); //This is where it will break if we didn't connect

                // Registering remote server events
                this.remoteServer.KinectDeviceInfoReceived += new DeviceConnectivityInfo(eventProxy.HandleDeviceConnectionInfoEvent);
                this.remoteServer.ServiceClientInfoReceived += new ClientConnectivityInfo(eventProxy.HandleClientConnectionInfoEvent);
                this.remoteServer.ConnectionStatusInfoReceived += new ConnectionStatus(eventProxy.HandleConnectionStatusEvent);
                this.remoteServer.ShowNotificationReceived += new EventHandler<NotificationReceivedArgs>(eventProxy.HandleNotificationEvent);

                #endregion

                this.connected = true;
            }
            finally
            {
                if (lockAquired)
                {
                    { if (!this.connected) { this.Disconnect(); } }
                    Monitor.Exit(this._ConnectSync);
                }
            }
        }

        /// <summary>
        /// Disconnects from remoting server and unregister events.
        /// </summary>
        public void Disconnect()
        {
            lock (this._ConnectSync)
            {
                if (this.remoteServer != null)
                {
                    
                    if (this.eventProxy != null)
                    {
                        // Un registering remote server events 
                        { try { this.remoteServer.KinectDeviceInfoReceived -= eventProxy.HandleDeviceConnectionInfoEvent; } catch { } }
                        { try { this.remoteServer.ServiceClientInfoReceived -= eventProxy.HandleClientConnectionInfoEvent; } catch { } }
                        { try { this.remoteServer.ConnectionStatusInfoReceived -= eventProxy.HandleConnectionStatusEvent; } catch { } }
                        { try { this.remoteServer.ShowNotificationReceived -= eventProxy.HandleNotificationEvent; } catch { } }
                    }

                    this.remoteServer = null;
                }

                if (this.eventProxy != null)
                {
                    // Un registering proxy events 
                    { try { this.eventProxy.KinectDeviceInfoReceived -= new DeviceConnectivityInfo(eventProxy_DeviceConnectivityInfo); } catch { } }
                    { try { this.eventProxy.ServiceClientInfoReceived -= new ClientConnectivityInfo(eventProxy_ClientConnectivityInfo); } catch { } }
                    { try { this.eventProxy.ShowNotificationReceived -= new EventHandler<NotificationReceivedArgs>(eventProxy_ShowNotificationReceived); } catch { } }
                    { try { this.eventProxy.ConnectionStatusInfoReceived -= new ConnectionStatus(eventProxy_ConnectionStatusInfoReceived); } catch { } }

                    this.eventProxy = null;
                }

                // Un registering channel established with remoting server
                if (tcpChan != null && ChannelServices.RegisteredChannels.Contains(tcpChan)) { ChannelServices.UnregisterChannel(tcpChan); }

                this.connected = false;
            }
        }

        /// <summary>
        /// Sends command to remoting server.
        /// </summary>
        /// <param name="command">Client command.</param>
        public void SendCommand(ClientCommands command)
        {
            // If not connected with remoting server
            if (!connected) { throw new Exception("Not connected with server"); }

            remoteServer.CommandFromClient(command, this.loginName, this.password);
        }

        /// <summary>
        /// Releases all the resources.
        /// </summary>
        public void Dispose()
        {
            if (this.updateServerConnectionStatus != null)
            {
                this.updateServerConnectionStatus.Stop();
                this.updateServerConnectionStatus.Dispose();
                this.updateServerConnectionStatus = null;
            }

            { try { this.Disconnect(); } catch { } }
        }

        #endregion
    }
}
